﻿using System;
using System.Collections.Generic;
using System.IO;
using System.Text.RegularExpressions;
using System.Threading.Tasks;

namespace Red.Core
{
    /// <summary>
    /// A variable in the form $name$ in a file path, that can resolve to a useful value
    /// </summary>
    public interface IFilePathCustomVariable
    {
        /// <summary>
        /// Name of the variable
        /// </summary>
        /// <remarks>
        /// Does not include the surrounding '$'
        /// </remarks>
        string Name { get; }

        /// <summary>
        /// A brief description of what the variable is useful for
        /// </summary>
        string Help { get; }

        /// <summary>
        /// The string that will replace the variable in the originating path
        /// </summary>
        string Value { get; }
    }

    namespace FilePathCustomVariables
    {
        /// <summary>
        /// This variable resolves to the root path of the local HDD/SSD drive with the most available space
        /// </summary>
        /// <remarks>
        /// This searches "fixed" drives, meaning not networked, not usb/removable etc
        /// Be aware, if the drive is mounted to an arbitrary sub directory, (so instead of C:\, it's C:\some\path\)
        /// that path will be returned in full. So if you have a drive for a specific purpose mounted to a sub dir
        /// it can still be chosen by this algorithm
        /// </remarks>
        internal class BestGuessDrive : IFilePathCustomVariable
        {
            private class LocalDriveInfo
            {
                public string Root { get; set; }
                public long FreeSpace { get; set; }
            }

            private LocalDriveInfo ConvertToLocalDriveInfo( DriveInfo drive )
            {
                if ( drive.IsReady && drive.DriveType == DriveType.Fixed )
                {
                    return new LocalDriveInfo()
                    {
                        Root = drive.RootDirectory.FullName,
                        FreeSpace = drive.TotalFreeSpace
                    };
                }

                return null;
            }

            private LocalDriveInfo FindBestDrive()
            {
                DriveInfo[] drives = DriveInfo.GetDrives();
                Task<LocalDriveInfo>[] tasks = new Task<LocalDriveInfo>[ drives.Length ];

                for ( int i = 0; i < drives.Length; ++i )
                {
                    int index = i;
                    tasks[ i ] = new Task<LocalDriveInfo>( () => { return ConvertToLocalDriveInfo( drives[ index ] ); } );
                    tasks[ i ].Start();
                }

                Task.WaitAll( tasks, 100 );

                LocalDriveInfo bestDrive = null;
                for ( int i = 0; i < drives.Length; ++i )
                {
                    LocalDriveInfo current = tasks[ i ].Result;

                    if( bestDrive == null || ( current != null && bestDrive.FreeSpace < current.FreeSpace ) )
                    {
                        bestDrive = current;
                    }
                }

                return bestDrive;
            }

            #region IFilePathCustomVariable

            public string Help => "HDD with largest free space";

            public string Name => "BestGuessDrive";

            public string Value => FindBestDrive()?.Root.TrimEnd( Path.DirectorySeparatorChar ) ?? "No drives available";

            #endregion IFilePathCustomVariable
        }
    }

    public class FilePath
    {
        #region Public Properties

        /// <summary>
        /// The directory string
        /// </summary>
        public string Path { get; set; }

        /// <summary>
        /// The directory string with all environmental and custom variables resolved to their respective values
        /// </summary>
        public string ResolvedPath { get { return Resolved(); } }

        public static IEnumerable<IFilePathCustomVariable> Variables => CustomVariables.Values;

        #endregion Public Properties

        #region Public Methods

        /// <summary>
        /// Register a new custom variable that can be resolved by any FilePath class
        /// </summary>
        /// <param name="var">An implementation (and instantication) of the <see cref="IFilePathCustomVariable"/> interface</param>
        public static void Register( IFilePathCustomVariable var )
        {
            CustomVariables[ var.Name ] = var;
        }

        #endregion Public Methods

        #region C-Tor

        static FilePath()
        {
            Register( new FilePathCustomVariables.BestGuessDrive() );
        }

        public FilePath() { }
        public FilePath( string path ) { Path = path; }

        #endregion C-Tor
        
        #region Private Properties

        private static Dictionary<string, IFilePathCustomVariable> CustomVariables { get; set; } = new Dictionary<string, IFilePathCustomVariable>();

        #endregion Private Properties

        #region Private Methods

        private string Resolved()
        {
            if ( Path == null )
                return null;

            string resolvedPath;

            resolvedPath = Environment.ExpandEnvironmentVariables( Path );
            resolvedPath = ResolvedCustomVariables( resolvedPath );

            return resolvedPath;
        }

        private static string ResolvedCustomVariables( string path )
        {
            // Find all custom variables in the path
            string regex = @"\$([\w_\.]*)\$";
            MatchCollection matches = Regex.Matches( path, regex );

            foreach ( Match match in matches )
            {
                if ( match.Success )
                {
                    string decorated = match.Value;
                    string raw = match.Groups[ 1 ].Value;

                    IFilePathCustomVariable var;
                    if ( CustomVariables.TryGetValue( raw, out var ) )
                    {
                        path = path.Replace( decorated, var.Value );
                    }
                }
            }

            return path;
        }

        #endregion Private Methods

        #region String Conversions

        /// <remarks>
        /// Implicit string conversions do not resolve variables
        /// </remarks>
        public override string ToString()
        {
            return Path;
        }

        public static implicit operator FilePath( string str )
        {
            return new FilePath( str );
        }

        /// <remarks>
        /// Implicit string conversions do not resolve variables
        /// </remarks>
        public static implicit operator string( FilePath path )
        {
            return path.Path;
        }

        #endregion String Conversions
    }
}
